{
 "cells": [
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:09.877186Z",
     "start_time": "2025-01-20T14:26:09.870555Z"
    }
   },
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "    \n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n",
    "\n",
    "seed = 42\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=12, micro=3, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.0\n",
      "torch 2.5.1+cpu\n",
      "cpu\n"
     ]
    }
   ],
   "execution_count": 16
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 数据准备\n",
    "\n",
    "```shell\n",
    "$ tree -L 2 archive \n",
    "archive\n",
    "├── monkey_labels.txt\n",
    "├── training\n",
    "│   ├── n0\n",
    "│   ├── n1\n",
    "│   ├── n2\n",
    "│   ├── n3\n",
    "│   ├── n4\n",
    "│   ├── n5\n",
    "│   ├── n6\n",
    "│   ├── n7\n",
    "│   ├── n8\n",
    "│   └── n9\n",
    "└── validation\n",
    "    ├── n0\n",
    "    ├── n1\n",
    "    ├── n2\n",
    "    ├── n3\n",
    "    ├── n4\n",
    "    ├── n5\n",
    "    ├── n6\n",
    "    ├── n7\n",
    "    ├── n8\n",
    "    └── n9\n",
    "\n",
    "22 directories, 1 file\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:09.987382Z",
     "start_time": "2025-01-20T14:26:09.974716Z"
    }
   },
   "source": [
    "from torchvision import datasets\n",
    "from torchvision.transforms import ToTensor, Resize, Compose, ConvertImageDtype, Normalize\n",
    "\n",
    "\n",
    "from pathlib import Path\n",
    "\n",
    "DATA_DIR = Path(\"./archive/\")\n",
    "\n",
    "class MonkeyDataset(datasets.ImageFolder):\n",
    "    def __init__(self, mode, transform=None):\n",
    "        if mode == \"train\":\n",
    "            root = DATA_DIR / \"training\"\n",
    "        elif mode == \"val\":\n",
    "            root = DATA_DIR / \"validation\"\n",
    "        else:\n",
    "            raise ValueError(\"mode should be one of the following: train, val, but got {}\".format(mode))\n",
    "        super().__init__(root, transform)\n",
    "        self.imgs = self.samples\n",
    "        self.targets = [s[1] for s in self.samples]\n",
    "\n",
    "# resnet 要求的，见 https://pytorch.org/vision/stable/models/generated/torchvision.models.resnet50.html\n",
    "img_h, img_w = 224, 224\n",
    "transform = Compose([\n",
    "     Resize((img_h, img_w)),\n",
    "     ToTensor(),\n",
    "     Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),\n",
    "     ConvertImageDtype(torch.float),\n",
    "])\n",
    "\n",
    "\n",
    "train_ds = MonkeyDataset(\"train\", transform=transform)\n",
    "val_ds = MonkeyDataset(\"val\", transform=transform)\n",
    "\n",
    "print(\"load {} images from training dataset\".format(len(train_ds)))\n",
    "print(\"load {} images from validation dataset\".format(len(val_ds)))"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "load 1097 images from training dataset\n",
      "load 272 images from validation dataset\n"
     ]
    }
   ],
   "execution_count": 17
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:09.992524Z",
     "start_time": "2025-01-20T14:26:09.988384Z"
    }
   },
   "source": [
    "# 数据类别\n",
    "train_ds.classes, train_ds.class_to_idx"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(['n0', 'n1', 'n2', 'n3', 'n4', 'n5', 'n6', 'n7', 'n8', 'n9'],\n",
       " {'n0': 0,\n",
       "  'n1': 1,\n",
       "  'n2': 2,\n",
       "  'n3': 3,\n",
       "  'n4': 4,\n",
       "  'n5': 5,\n",
       "  'n6': 6,\n",
       "  'n7': 7,\n",
       "  'n8': 8,\n",
       "  'n9': 9})"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 18
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:10.000889Z",
     "start_time": "2025-01-20T14:26:09.993524Z"
    }
   },
   "source": [
    "# 图片路径 及 标签\n",
    "for fpath, label in train_ds.imgs:\n",
    "    print(fpath, label)\n",
    "    break\n",
    "\n",
    "for img, label in train_ds:\n",
    "    # c, h, w  label\n",
    "    print(img.shape, label)\n",
    "    break"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "archive\\training\\n0\\n0018.jpg 0\n",
      "torch.Size([3, 224, 224]) 0\n"
     ]
    }
   ],
   "execution_count": 19
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:10.010938Z",
     "start_time": "2025-01-20T14:26:10.006878Z"
    }
   },
   "source": [
    "# 遍历train_ds得到每张图片，计算每个通道的均值和方差\n",
    "def cal_mean_std(ds):\n",
    "    mean = 0.\n",
    "    std = 0.\n",
    "    for img, _ in ds:\n",
    "        mean += img.mean(dim=(1, 2))\n",
    "        std += img.std(dim=(1, 2))\n",
    "    mean /= len(ds)\n",
    "    std /= len(ds)\n",
    "    return mean, std\n",
    "\n",
    "# 经过 normalize 后 均值为0，方差为1\n",
    "# print(cal_mean_std(train_ds))"
   ],
   "outputs": [],
   "execution_count": 20
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:10.057212Z",
     "start_time": "2025-01-20T14:26:10.052130Z"
    }
   },
   "source": [
    "import torch.nn as nn\n",
    "from torch.utils.data.dataloader import DataLoader    \n",
    "\n",
    "batch_size = 16\n",
    "# 从数据集到dataloader\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)\n",
    "val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False)"
   ],
   "outputs": [],
   "execution_count": 21
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:10.522154Z",
     "start_time": "2025-01-20T14:26:10.070179Z"
    }
   },
   "source": [
    "for imgs, labels in train_loader:\n",
    "    print(imgs.shape)\n",
    "    print(labels.shape)\n",
    "    break"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "torch.Size([16, 3, 224, 224])\n",
      "torch.Size([16])\n"
     ]
    }
   ],
   "execution_count": 22
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型"
   ]
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "权重默认下载路径\n",
    "C:\\Users\\54439\\.cache\\torch\\hub\\checkpoints"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:11.026761Z",
     "start_time": "2025-01-20T14:26:10.522657Z"
    }
   },
   "source": [
    "from torchvision.models import resnet50\n",
    "\n",
    "\n",
    "# 加载resnet50模型以及预训练权重，冻结除最后一层外所有层的权重，去除最后一层并添加自定义分类层\n",
    "class ResNet50(nn.Module):\n",
    "    def __init__(self, num_classes=10, frozen=True):\n",
    "        super().__init__()\n",
    "        # 下载预训练权重\n",
    "        self.model = resnet50(weights=\"IMAGENET1K_V2\",) # 这里的weights参数可以选择\"IMAGENET1K_V2\"或None，None表示随机初始化\n",
    "        # 冻结前面的层\n",
    "        if frozen:\n",
    "            for param in self.model.parameters():\n",
    "                param.requires_grad = False # 冻结权重\n",
    "        # for param in self.model.layer4.parameters():\n",
    "        #     param.requires_grad = True  # 解冻 layer4\n",
    "        for name, param in self.model.named_parameters():\n",
    "            if name == \"layer4.2.conv3.weight\":\n",
    "                param.requires_grad = True  # 解冻该层\n",
    "        # 添加自定义分类层\n",
    "        # print(self.model)\n",
    "        print(self.model.fc.in_features) # 打印resnet50的最后一层的输入通道数\n",
    "        print(self.model.fc.out_features) # 打印resnet50的最后一层的输出通道数 1000\n",
    "        self.model.fc = nn.Linear(self.model.fc.in_features, num_classes)   # 自定义分类层,把resnet50的最后一层改为num_classes个输出\n",
    "        \n",
    "        \n",
    "    def forward(self, x):\n",
    "        return self.model(x)\n",
    "\n",
    "\n",
    "for idx, (key, value) in enumerate(ResNet50().named_parameters()):\n",
    "    print(f\"{key:^40}paramerters num: {np.prod(value.shape)}\")\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n",
      "           model.conv1.weight           paramerters num: 9408\n",
      "            model.bn1.weight            paramerters num: 64\n",
      "             model.bn1.bias             paramerters num: 64\n",
      "      model.layer1.0.conv1.weight       paramerters num: 4096\n",
      "       model.layer1.0.bn1.weight        paramerters num: 64\n",
      "        model.layer1.0.bn1.bias         paramerters num: 64\n",
      "      model.layer1.0.conv2.weight       paramerters num: 36864\n",
      "       model.layer1.0.bn2.weight        paramerters num: 64\n",
      "        model.layer1.0.bn2.bias         paramerters num: 64\n",
      "      model.layer1.0.conv3.weight       paramerters num: 16384\n",
      "       model.layer1.0.bn3.weight        paramerters num: 256\n",
      "        model.layer1.0.bn3.bias         paramerters num: 256\n",
      "   model.layer1.0.downsample.0.weight   paramerters num: 16384\n",
      "   model.layer1.0.downsample.1.weight   paramerters num: 256\n",
      "    model.layer1.0.downsample.1.bias    paramerters num: 256\n",
      "      model.layer1.1.conv1.weight       paramerters num: 16384\n",
      "       model.layer1.1.bn1.weight        paramerters num: 64\n",
      "        model.layer1.1.bn1.bias         paramerters num: 64\n",
      "      model.layer1.1.conv2.weight       paramerters num: 36864\n",
      "       model.layer1.1.bn2.weight        paramerters num: 64\n",
      "        model.layer1.1.bn2.bias         paramerters num: 64\n",
      "      model.layer1.1.conv3.weight       paramerters num: 16384\n",
      "       model.layer1.1.bn3.weight        paramerters num: 256\n",
      "        model.layer1.1.bn3.bias         paramerters num: 256\n",
      "      model.layer1.2.conv1.weight       paramerters num: 16384\n",
      "       model.layer1.2.bn1.weight        paramerters num: 64\n",
      "        model.layer1.2.bn1.bias         paramerters num: 64\n",
      "      model.layer1.2.conv2.weight       paramerters num: 36864\n",
      "       model.layer1.2.bn2.weight        paramerters num: 64\n",
      "        model.layer1.2.bn2.bias         paramerters num: 64\n",
      "      model.layer1.2.conv3.weight       paramerters num: 16384\n",
      "       model.layer1.2.bn3.weight        paramerters num: 256\n",
      "        model.layer1.2.bn3.bias         paramerters num: 256\n",
      "      model.layer2.0.conv1.weight       paramerters num: 32768\n",
      "       model.layer2.0.bn1.weight        paramerters num: 128\n",
      "        model.layer2.0.bn1.bias         paramerters num: 128\n",
      "      model.layer2.0.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.0.bn2.weight        paramerters num: 128\n",
      "        model.layer2.0.bn2.bias         paramerters num: 128\n",
      "      model.layer2.0.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.0.bn3.weight        paramerters num: 512\n",
      "        model.layer2.0.bn3.bias         paramerters num: 512\n",
      "   model.layer2.0.downsample.0.weight   paramerters num: 131072\n",
      "   model.layer2.0.downsample.1.weight   paramerters num: 512\n",
      "    model.layer2.0.downsample.1.bias    paramerters num: 512\n",
      "      model.layer2.1.conv1.weight       paramerters num: 65536\n",
      "       model.layer2.1.bn1.weight        paramerters num: 128\n",
      "        model.layer2.1.bn1.bias         paramerters num: 128\n",
      "      model.layer2.1.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.1.bn2.weight        paramerters num: 128\n",
      "        model.layer2.1.bn2.bias         paramerters num: 128\n",
      "      model.layer2.1.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.1.bn3.weight        paramerters num: 512\n",
      "        model.layer2.1.bn3.bias         paramerters num: 512\n",
      "      model.layer2.2.conv1.weight       paramerters num: 65536\n",
      "       model.layer2.2.bn1.weight        paramerters num: 128\n",
      "        model.layer2.2.bn1.bias         paramerters num: 128\n",
      "      model.layer2.2.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.2.bn2.weight        paramerters num: 128\n",
      "        model.layer2.2.bn2.bias         paramerters num: 128\n",
      "      model.layer2.2.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.2.bn3.weight        paramerters num: 512\n",
      "        model.layer2.2.bn3.bias         paramerters num: 512\n",
      "      model.layer2.3.conv1.weight       paramerters num: 65536\n",
      "       model.layer2.3.bn1.weight        paramerters num: 128\n",
      "        model.layer2.3.bn1.bias         paramerters num: 128\n",
      "      model.layer2.3.conv2.weight       paramerters num: 147456\n",
      "       model.layer2.3.bn2.weight        paramerters num: 128\n",
      "        model.layer2.3.bn2.bias         paramerters num: 128\n",
      "      model.layer2.3.conv3.weight       paramerters num: 65536\n",
      "       model.layer2.3.bn3.weight        paramerters num: 512\n",
      "        model.layer2.3.bn3.bias         paramerters num: 512\n",
      "      model.layer3.0.conv1.weight       paramerters num: 131072\n",
      "       model.layer3.0.bn1.weight        paramerters num: 256\n",
      "        model.layer3.0.bn1.bias         paramerters num: 256\n",
      "      model.layer3.0.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.0.bn2.weight        paramerters num: 256\n",
      "        model.layer3.0.bn2.bias         paramerters num: 256\n",
      "      model.layer3.0.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.0.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.0.bn3.bias         paramerters num: 1024\n",
      "   model.layer3.0.downsample.0.weight   paramerters num: 524288\n",
      "   model.layer3.0.downsample.1.weight   paramerters num: 1024\n",
      "    model.layer3.0.downsample.1.bias    paramerters num: 1024\n",
      "      model.layer3.1.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.1.bn1.weight        paramerters num: 256\n",
      "        model.layer3.1.bn1.bias         paramerters num: 256\n",
      "      model.layer3.1.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.1.bn2.weight        paramerters num: 256\n",
      "        model.layer3.1.bn2.bias         paramerters num: 256\n",
      "      model.layer3.1.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.1.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.1.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.2.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.2.bn1.weight        paramerters num: 256\n",
      "        model.layer3.2.bn1.bias         paramerters num: 256\n",
      "      model.layer3.2.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.2.bn2.weight        paramerters num: 256\n",
      "        model.layer3.2.bn2.bias         paramerters num: 256\n",
      "      model.layer3.2.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.2.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.2.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.3.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.3.bn1.weight        paramerters num: 256\n",
      "        model.layer3.3.bn1.bias         paramerters num: 256\n",
      "      model.layer3.3.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.3.bn2.weight        paramerters num: 256\n",
      "        model.layer3.3.bn2.bias         paramerters num: 256\n",
      "      model.layer3.3.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.3.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.3.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.4.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.4.bn1.weight        paramerters num: 256\n",
      "        model.layer3.4.bn1.bias         paramerters num: 256\n",
      "      model.layer3.4.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.4.bn2.weight        paramerters num: 256\n",
      "        model.layer3.4.bn2.bias         paramerters num: 256\n",
      "      model.layer3.4.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.4.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.4.bn3.bias         paramerters num: 1024\n",
      "      model.layer3.5.conv1.weight       paramerters num: 262144\n",
      "       model.layer3.5.bn1.weight        paramerters num: 256\n",
      "        model.layer3.5.bn1.bias         paramerters num: 256\n",
      "      model.layer3.5.conv2.weight       paramerters num: 589824\n",
      "       model.layer3.5.bn2.weight        paramerters num: 256\n",
      "        model.layer3.5.bn2.bias         paramerters num: 256\n",
      "      model.layer3.5.conv3.weight       paramerters num: 262144\n",
      "       model.layer3.5.bn3.weight        paramerters num: 1024\n",
      "        model.layer3.5.bn3.bias         paramerters num: 1024\n",
      "      model.layer4.0.conv1.weight       paramerters num: 524288\n",
      "       model.layer4.0.bn1.weight        paramerters num: 512\n",
      "        model.layer4.0.bn1.bias         paramerters num: 512\n",
      "      model.layer4.0.conv2.weight       paramerters num: 2359296\n",
      "       model.layer4.0.bn2.weight        paramerters num: 512\n",
      "        model.layer4.0.bn2.bias         paramerters num: 512\n",
      "      model.layer4.0.conv3.weight       paramerters num: 1048576\n",
      "       model.layer4.0.bn3.weight        paramerters num: 2048\n",
      "        model.layer4.0.bn3.bias         paramerters num: 2048\n",
      "   model.layer4.0.downsample.0.weight   paramerters num: 2097152\n",
      "   model.layer4.0.downsample.1.weight   paramerters num: 2048\n",
      "    model.layer4.0.downsample.1.bias    paramerters num: 2048\n",
      "      model.layer4.1.conv1.weight       paramerters num: 1048576\n",
      "       model.layer4.1.bn1.weight        paramerters num: 512\n",
      "        model.layer4.1.bn1.bias         paramerters num: 512\n",
      "      model.layer4.1.conv2.weight       paramerters num: 2359296\n",
      "       model.layer4.1.bn2.weight        paramerters num: 512\n",
      "        model.layer4.1.bn2.bias         paramerters num: 512\n",
      "      model.layer4.1.conv3.weight       paramerters num: 1048576\n",
      "       model.layer4.1.bn3.weight        paramerters num: 2048\n",
      "        model.layer4.1.bn3.bias         paramerters num: 2048\n",
      "      model.layer4.2.conv1.weight       paramerters num: 1048576\n",
      "       model.layer4.2.bn1.weight        paramerters num: 512\n",
      "        model.layer4.2.bn1.bias         paramerters num: 512\n",
      "      model.layer4.2.conv2.weight       paramerters num: 2359296\n",
      "       model.layer4.2.bn2.weight        paramerters num: 512\n",
      "        model.layer4.2.bn2.bias         paramerters num: 512\n",
      "      model.layer4.2.conv3.weight       paramerters num: 1048576\n",
      "       model.layer4.2.bn3.weight        paramerters num: 2048\n",
      "        model.layer4.2.bn3.bias         paramerters num: 2048\n",
      "            model.fc.weight             paramerters num: 20480\n",
      "             model.fc.bias              paramerters num: 10\n"
     ]
    }
   ],
   "execution_count": 23
  },
  {
   "cell_type": "code",
   "source": [
    "model = ResNet50(num_classes=10, frozen=True)\n",
    "def count_parameters(model): #计算模型总参数量\n",
    "    return sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
    "count_parameters(model)"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:11.400213Z",
     "start_time": "2025-01-20T14:26:11.027761Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "1069066"
      ]
     },
     "execution_count": 24,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 24
  },
  {
   "cell_type": "code",
   "source": "model",
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:11.408172Z",
     "start_time": "2025-01-20T14:26:11.401212Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "ResNet50(\n",
       "  (model): ResNet(\n",
       "    (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)\n",
       "    (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "    (relu): ReLU(inplace=True)\n",
       "    (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)\n",
       "    (layer1): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "          (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer2): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (3): Bottleneck(\n",
       "        (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer3): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (3): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (4): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (5): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (layer4): Sequential(\n",
       "      (0): Bottleneck(\n",
       "        (conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "        (downsample): Sequential(\n",
       "          (0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)\n",
       "          (1): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        )\n",
       "      )\n",
       "      (1): Bottleneck(\n",
       "        (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "      (2): Bottleneck(\n",
       "        (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)\n",
       "        (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)\n",
       "        (bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)\n",
       "        (relu): ReLU(inplace=True)\n",
       "      )\n",
       "    )\n",
       "    (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))\n",
       "    (fc): Linear(in_features=2048, out_features=10, bias=True)\n",
       "  )\n",
       ")"
      ]
     },
     "execution_count": 25,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 25
  },
  {
   "cell_type": "code",
   "source": [
    "total_params = sum(p.numel() for p in model.parameters() )\n",
    "print(f\"Total trainable parameters: {total_params}\")"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:11.413580Z",
     "start_time": "2025-01-20T14:26:11.409177Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total trainable parameters: 23528522\n"
     ]
    }
   ],
   "execution_count": 26
  },
  {
   "cell_type": "code",
   "source": [
    "m = nn.AdaptiveAvgPool2d(output_size=(1, 1))\n",
    "input = torch.randn(1, 2048, 9, 9)\n",
    "output = m(input)\n",
    "output.shape"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:11.420174Z",
     "start_time": "2025-01-20T14:26:11.414583Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([1, 2048, 1, 1])"
      ]
     },
     "execution_count": 27,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 27
  },
  {
   "cell_type": "code",
   "source": [
    "512*3*3*512"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:11.425570Z",
     "start_time": "2025-01-20T14:26:11.421179Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "2359296"
      ]
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 28
  },
  {
   "cell_type": "code",
   "source": [
    "512*1*1*2048"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:11.430847Z",
     "start_time": "2025-01-20T14:26:11.426570Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1048576"
      ]
     },
     "execution_count": 29,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 29
  },
  {
   "cell_type": "code",
   "source": [
    "512/16"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:11.435706Z",
     "start_time": "2025-01-20T14:26:11.431846Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "32.0"
      ]
     },
     "execution_count": 30,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 30
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 训练\n",
    "\n",
    "pytorch的训练需要自行实现，包括\n",
    "1. 定义损失函数\n",
    "2. 定义优化器\n",
    "3. 定义训练步\n",
    "4. 训练"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:11.916732Z",
     "start_time": "2025-01-20T14:26:11.437704Z"
    }
   },
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "@torch.no_grad()\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []\n",
    "    pred_list = []\n",
    "    label_list = []\n",
    "    for datas, labels in dataloader:\n",
    "        datas = datas.to(device)\n",
    "        labels = labels.to(device)\n",
    "        # 前向计算\n",
    "        logits = model(datas)\n",
    "        loss = loss_fct(logits, labels)         # 验证集损失\n",
    "        loss_list.append(loss.item())\n",
    "        \n",
    "        preds = logits.argmax(axis=-1)    # 验证集预测\n",
    "        pred_list.extend(preds.cpu().numpy().tolist())\n",
    "        label_list.extend(labels.cpu().numpy().tolist())\n",
    "        \n",
    "    acc = accuracy_score(label_list, pred_list)\n",
    "    return np.mean(loss_list), acc\n"
   ],
   "outputs": [],
   "execution_count": 31
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### TensorBoard 可视化\n",
    "\n",
    "\n",
    "训练过程中可以使用如下命令启动tensorboard服务。\n",
    "\n",
    "```shell\n",
    "tensorboard \\\n",
    "    --logdir=runs \\     # log 存放路径\n",
    "    --host 0.0.0.0 \\    # ip\n",
    "    --port 8848         # 端口\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:22.083061Z",
     "start_time": "2025-01-20T14:26:11.918736Z"
    }
   },
   "source": [
    "from torch.utils.tensorboard import SummaryWriter\n",
    "\n",
    "\n",
    "class TensorBoardCallback:\n",
    "    def __init__(self, log_dir, flush_secs=10):\n",
    "        \"\"\"\n",
    "        Args:\n",
    "            log_dir (str): dir to write log.\n",
    "            flush_secs (int, optional): write to dsk each flush_secs seconds. Defaults to 10.\n",
    "        \"\"\"\n",
    "        self.writer = SummaryWriter(log_dir=log_dir, flush_secs=flush_secs)\n",
    "\n",
    "    def draw_model(self, model, input_shape):\n",
    "        self.writer.add_graph(model, input_to_model=torch.randn(input_shape))\n",
    "        \n",
    "    def add_loss_scalars(self, step, loss, val_loss):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/loss\", \n",
    "            tag_scalar_dict={\"loss\": loss, \"val_loss\": val_loss},\n",
    "            global_step=step,\n",
    "            )\n",
    "        \n",
    "    def add_acc_scalars(self, step, acc, val_acc):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/accuracy\",\n",
    "            tag_scalar_dict={\"accuracy\": acc, \"val_accuracy\": val_acc},\n",
    "            global_step=step,\n",
    "        )\n",
    "        \n",
    "    def add_lr_scalars(self, step, learning_rate):\n",
    "        self.writer.add_scalars(\n",
    "            main_tag=\"training/learning_rate\",\n",
    "            tag_scalar_dict={\"learning_rate\": learning_rate},\n",
    "            global_step=step,\n",
    "            \n",
    "        )\n",
    "    \n",
    "    def __call__(self, step, **kwargs):\n",
    "        # add loss\n",
    "        loss = kwargs.pop(\"loss\", None)\n",
    "        val_loss = kwargs.pop(\"val_loss\", None)\n",
    "        if loss is not None and val_loss is not None:\n",
    "            self.add_loss_scalars(step, loss, val_loss)\n",
    "        # add acc\n",
    "        acc = kwargs.pop(\"acc\", None)\n",
    "        val_acc = kwargs.pop(\"val_acc\", None)\n",
    "        if acc is not None and val_acc is not None:\n",
    "            self.add_acc_scalars(step, acc, val_acc)\n",
    "        # add lr\n",
    "        learning_rate = kwargs.pop(\"lr\", None)\n",
    "        if learning_rate is not None:\n",
    "            self.add_lr_scalars(step, learning_rate)\n"
   ],
   "outputs": [],
   "execution_count": 32
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Save Best\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:22.089176Z",
     "start_time": "2025-01-20T14:26:22.083818Z"
    }
   },
   "source": [
    "class SaveCheckpointsCallback:\n",
    "    def __init__(self, save_dir, save_step=5000, save_best_only=True):\n",
    "        \"\"\"\n",
    "        Save checkpoints each save_epoch epoch. \n",
    "        We save checkpoint by epoch in this implementation.\n",
    "        Usually, training scripts with pytorch evaluating model and save checkpoint by step.\n",
    "\n",
    "        Args:\n",
    "            save_dir (str): dir to save checkpoint\n",
    "            save_epoch (int, optional): the frequency to save checkpoint. Defaults to 1.\n",
    "            save_best_only (bool, optional): If True, only save the best model or save each model at every epoch.\n",
    "        \"\"\"\n",
    "        self.save_dir = save_dir\n",
    "        self.save_step = save_step\n",
    "        self.save_best_only = save_best_only\n",
    "        self.best_metrics = -1\n",
    "        \n",
    "        # mkdir\n",
    "        if not os.path.exists(self.save_dir):\n",
    "            os.mkdir(self.save_dir)\n",
    "        \n",
    "    def __call__(self, step, state_dict, metric=None):\n",
    "        if step % self.save_step > 0:\n",
    "            return\n",
    "        \n",
    "        if self.save_best_only:\n",
    "            assert metric is not None\n",
    "            if metric >= self.best_metrics:\n",
    "                # save checkpoints\n",
    "                torch.save(state_dict, os.path.join(self.save_dir, \"best.ckpt\"))\n",
    "                # update best metrics\n",
    "                self.best_metrics = metric\n",
    "        else:\n",
    "            torch.save(state_dict, os.path.join(self.save_dir, f\"{step}.ckpt\"))\n",
    "\n"
   ],
   "outputs": [],
   "execution_count": 33
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Early Stop"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T14:26:22.096868Z",
     "start_time": "2025-01-20T14:26:22.090176Z"
    }
   },
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        \"\"\"\n",
    "\n",
    "        Args:\n",
    "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
    "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute \n",
    "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
    "        \"\"\"\n",
    "        self.patience = patience\n",
    "        self.min_delta = min_delta\n",
    "        self.best_metric = -1\n",
    "        self.counter = 0\n",
    "        \n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            # update best metric\n",
    "            self.best_metric = metric\n",
    "            # reset counter \n",
    "            self.counter = 0\n",
    "        else: \n",
    "            self.counter += 1\n",
    "            \n",
    "    @property\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience\n"
   ],
   "outputs": [],
   "execution_count": 34
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T14:53:12.432121Z",
     "start_time": "2025-01-20T14:26:22.097868Z"
    }
   },
   "source": [
    "# 训练\n",
    "def training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=None,\n",
    "    early_stop_callback=None,\n",
    "    eval_step=500,\n",
    "    ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "    \n",
    "    global_step = 0\n",
    "    model.train()\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits = model(datas)\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits, labels)\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    "                preds = logits.argmax(axis=-1)\n",
    "            \n",
    "                acc = accuracy_score(labels.cpu().numpy(), preds.cpu().numpy())    \n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "                \n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"acc\": acc, \"step\": global_step\n",
    "                })\n",
    "                \n",
    "                # evaluating\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()\n",
    "                    val_loss, val_acc = evaluating(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"acc\": val_acc, \"step\": global_step\n",
    "                    })\n",
    "                    model.train()\n",
    "                    \n",
    "                    # 1. 使用 tensorboard 可视化\n",
    "                    if tensorboard_callback is not None:\n",
    "                        tensorboard_callback(\n",
    "                            global_step, \n",
    "                            loss=loss, val_loss=val_loss,\n",
    "                            acc=acc, val_acc=val_acc,\n",
    "                            lr=optimizer.param_groups[0][\"lr\"],\n",
    "                            )\n",
    "                \n",
    "                    # 2. 保存模型权重 save model checkpoint\n",
    "                    if save_ckpt_callback is not None:\n",
    "                        save_ckpt_callback(global_step, model.state_dict(), metric=val_acc)\n",
    "\n",
    "                    # 3. 早停 Early Stop\n",
    "                    if early_stop_callback is not None:\n",
    "                        early_stop_callback(val_acc)\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "                    \n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "        \n",
    "    return record_dict\n",
    "        \n",
    "\n",
    "epoch = 20\n",
    "\n",
    "model = ResNet50(num_classes=10)\n",
    "\n",
    "# 1. 定义损失函数 采用交叉熵损失\n",
    "loss_fct = nn.CrossEntropyLoss()\n",
    "# 2. 定义优化器 采用 sgd\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.0)\n",
    "\n",
    "# 1. tensorboard 可视化\n",
    "if not os.path.exists(\"runs\"):\n",
    "    os.mkdir(\"runs\")\n",
    "# tensorboard_callback = TensorBoardCallback(\"runs/monkeys-resnet50\")\n",
    "# tensorboard_callback.draw_model(model, [1, 3, img_h, img_w])\n",
    "# 2. save best\n",
    "if not os.path.exists(\"checkpoints\"):\n",
    "    os.makedirs(\"checkpoints\")\n",
    "save_ckpt_callback = SaveCheckpointsCallback(\"checkpoints/monkeys-resnet50\", save_step=len(train_loader), save_best_only=True)\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=5)\n",
    "\n",
    "model = model.to(device)\n",
    "record = training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=save_ckpt_callback,\n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    "    )"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2048\n",
      "1000\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "  0%|          | 0/1380 [00:00<?, ?it/s]"
      ],
      "application/vnd.jupyter.widget-view+json": {
       "version_major": 2,
       "version_minor": 0,
       "model_id": "5947a3dfb4d24beb9947dc8293c7d132"
      }
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Early stop at epoch 13 / global_step 897\n"
     ]
    }
   ],
   "execution_count": 35
  },
  {
   "cell_type": "code",
   "source": [
    "# 画图\n",
    "\n",
    "import torch\n",
    "from torchviz import make_dot\n",
    "\n",
    "# Assuming your model is already defined and named 'model'\n",
    "# Construct a dummy input\n",
    "dummy_input = torch.randn(1, 3, 224, 224)  # Replace with your input shape\n",
    "\n",
    "# Forward pass to generate the computation graph\n",
    "output = model(dummy_input)\n",
    "\n",
    "# Visualize the model architecture\n",
    "dot = make_dot(output, params=dict(model.named_parameters()))\n",
    "dot.render(\"model_architecture\", format=\"png\")  # Save the visualization as an image\n"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-20T14:54:00.051859Z",
     "start_time": "2025-01-20T14:53:59.859523Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'model_architecture.png'"
      ]
     },
     "execution_count": 39,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 39
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T14:54:01.843508Z",
     "start_time": "2025-01-20T14:54:01.664321Z"
    }
   },
   "source": [
    "#画线要注意的是损失是不一定在零到1之间的\n",
    "def plot_learning_curves(record_dict, sample_step=500):\n",
    "    # build DataFrame\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    # plot\n",
    "    fig_num = len(train_df.columns)\n",
    "    fig, axs = plt.subplots(1, fig_num, figsize=(5 * fig_num, 5))\n",
    "    for idx, item in enumerate(train_df.columns):    \n",
    "        axs[idx].plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
    "        axs[idx].plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
    "        axs[idx].grid()\n",
    "        axs[idx].legend()\n",
    "        # axs[idx].set_xticks(range(0, train_df.index[-1], 5000))\n",
    "        # axs[idx].set_xticklabels(map(lambda x: f\"{int(x/1000)}k\", range(0, train_df.index[-1], 5000)))\n",
    "        axs[idx].set_xlabel(\"step\")\n",
    "    \n",
    "    plt.show()\n",
    "\n",
    "plot_learning_curves(record, sample_step=10)  #横坐标是 steps"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 1000x500 with 2 Axes>"
      ],
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAzoAAAHACAYAAABqJx3iAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAstdJREFUeJzs3Qd4W+XVB/C/JO+9R/beiyQkJGGTAQEKLauUsvfox2ih0LIpe7dAKVCgLVD2KoSQkEEI2XvvnXjvbVnS95z36sqSLdmyLFu29P89jyJZw7q+cqx7dM57jsFms9lAREREREQURIyB3gAiIiIiIiJ/Y6BDRERERERBh4EOEREREREFHQY6REREREQUdBjoEBERERFR0GGgQ0REREREQYeBDhERERERBR0GOkREREREFHTC0A1YrVYcO3YM8fHxMBgMgd4cIqKQITOlKyoq0KNHDxiN/GxMx/clIqKu/97ULQIdeTPp3bt3oDeDiChkHT58GL169Qr0ZnQZfF8iIur6703dItCRT8z0HyYhIaHNjzebzZg3bx5mzpyJ8PDwDtjC7ov7pmXcP55x34TGvikvL1cH9PrfYdLwfaljcf94xn3TMu6f0Ng35V6+N3WLQEcvC5A3E1/fUGJiYtRju/sL62/cNy3j/vGM+ya09g3Ls1zxfaljcf94xn3TMu6f0No3hlbem1hwTUREREREQYeBDhERERERBR0GOkREREREFHS6xRodIuq6GhoaYLFYAr0ZXa4OOiwsDLW1tV1+35hMJrWtXINDRETBhoEOEfl8MJ+SkoL9+/fzINlNf/+srCzVkas77BtZnJqdnY2IiIhAbwoREZHfMNAhIp+GJR46dAjJyclqWFdkZGS3OKDvzP1TWVmJuLi4Lj1kUwKy+vp6FBQUqIB18ODBXXp7iYiI2oKBDhG1mRwcy8F8enq6alPJg2NXsm9kH0VFRXX5fRMdHa3ajB48eNCxzURERMGga78DE1GXxixOcOjqwRgREZEv+O5GRERERERBh4EOEREREREFHQY6REQ+6tevH1566SW/fK/FixerUsDS0lK/fL9QsmTJEpx77rmqMYbswy+//NKr/T1+/HjVSGPQoEF49913O2VbiYio8zDQIaKQcuqpp+KOO+7wy/davXo1brjhBr98L/JdVVUVxo4di1dffdWr+0uHubPPPhunnXYaNmzYoH4frrvuOnz//fcdvq1ERNR52HWNiKhJy2UZ8ilDNFsjXeco8M466yx18tbrr7+O/v374/nnn1dfDx8+HEuXLsWLL76IWbNmdeCWEhFRZwr6QOfHXQV4Zu52xJiNmB3ojSEK4uCgxmwJyHNHh5u87v521VVX4ccff1Snl19+WV33zjvv4Oqrr8acOXNw//33Y/PmzZg3bx569+6Nu+66CytWrFAZAzkYfvLJJzF9+nSX0jXJBugZItmON998E9988436Hj179lQH07/4xS98+tk+++wzPPjgg9izZ48a6Pm73/0Ov//97x23v/baa+rgXAaTJiYm4qSTTsKnn36qbpPzRx55RD1WBoIed9xx+OqrrxAbG4tQt3z5cpfXUUiA01Kmr66uTp105eXljsG5cmor/TG+PNZXT83diaV7ijze3jc1Bi9eNAYRYa7FHt9tycWn647imQtGIzW25aGyT363E/UWKx48e1iz/5fvLDuI9YdK8fxFoxFu8lxQYrHacNfHG7F+vwmv7v251f/fkWFG3DNrCCb3T3G5vqS6Hnd+vBmFlY2vW1MXjO+Jq6f2bXb9X+bsgDzrn2cPa3bbW0sPYMvRcjx34SiENfk5vt2ci38s2Q+rzQZvTB2Yij+dNbTZ9W/8tB9fb8zx+Pe2otK7fdMWJqMBN53cH2eNynK5vq7Bit9/sgnH90vGlVP6NtuWh7/ZjrUHO77k9uTBaep1buq1xfswZ0uuT/snMTocT/9qFHolR7tcvyO3Ag/9bzuq6hravd2enmNbTjke/t92VNdb/PIcz14wCj2SXJ9j67FyPPJN43O0tm+yE6Pw8iVjEBPhGh4s2lmAvy7cC7PF2uZtk//r/3f6QJw21L8fDHr7tzPoAx35Y7P1WAV6xLANLlFHkSBnxIOBKfvZ9uisZn+UPZHgZteuXRg1ahQeffRRdd3WrVvV+b333ovnnnsOAwYMUINQJXiYPXs2Hn/8cbWO49///rdaB7Jz50706dPH43NIcPHUU0+pAEXWfVx22WVqRk1KiutBWGvWrl2Liy++GA8//DAuueQSLFu2DLfccgtSU1NVwLZmzRr83//9H/7zn/9g6tSpKC4uxk8//aQem5OTg0svvRTPPPMMfvnLX6KiokLdJm9yBOTm5iIzM9PlOvlagpeamho1W6gpCXLltW1KAloJJH01f/58dIZKM/DPNS3/P9mZV4lXP/4eQ5Ncf0+e22TC4SoDnv9oAU7M8vw7VGEG3rY/x0DzfqRENt4mv3rPrzahzmLAUMNR9I/3vB2HKoE5W+X7GJBTXeXVz/fMl6tw7VDXg7DleQb8vM/U4uNenr8DmaXa3wBdlRn4l/3nGFC/D4lOsZ3VBrywygSz1YBhhiPoE+f6/V7YZMKhKu+PN2SfD6jbi4Qmz/HSShPMtpa+j/f7pi2e/WYjbIfWuVy3o9SA77ebsGRnHtKKt8L5+LioFvhgfeccSmr7ag9inJ5Ojrv/usoES7N95f3+eemzxTi9h+vv9VcHjFiX47/VHc99shgze7k+x8f7jFif57/nePaTxZjR0/U5PnL7HJ73jezjlz+aj7Gprt/nla1G7C73fVuf/Xotaka2PUhqSXV1tVf3C/pAp7c9gi7x/IEOEYUIyXpERESoA9OsLO1Tyx07dqhzCXxmzJjhuK8EJrLuQ/fYY4/hiy++wNdff43bbrvN43NIECJBhhw0S5D0t7/9DatWrcKZZ57Zpm194YUXcMYZZ+CBBx5QXw8ZMgTbtm3Ds88+q57j0KFDKjtzzjnnID4+Hn379lVZGz3QaWhowK9+9St1vRg9enSbnp9c3XfffSrDp5PXV7J+M2fOVENzffk0UoIc+Z2Tga0dbdWBYmDNGmQmRKpPl5t6dfE+rD5QgtQBIzH7hMZA3mq14d41C+QSojP7Y7abDIdu5X7tOUTfUZNw0uA0x205ZbWoW7FEXR42ZiLOGJ7RYiUGNq9HepQNT144rsUyUvnk/am5u1BpjMfs2dNcbtv43U5g30GcNTITlxzfy+W2ytoG3PbhRlRbDJg560yXzMy+gipgzc/azzF6ssq66A6XVMO8Yqm6PHzc8ThliOun1M9ul5+xFg+dMwz901rOnt73xVa1X/qPPcElG3WwWHsOyaz947LjXAILIf+3161dh/ETxntVYuuN/PI63PP5FhSZw3DWWTNdPu3PX34Q2L4TNRYDJp18BtLjI10+6cf69eibEoNHfjEcHeXuTzejoLIeA8ZNxfg+SY7r9+RXwrJyGWIjTHj1N+PatH8+XH0Ec7fmIbPPQMye6ZopWvz5FiDnGC6Z2LNZhqstvtuSh4/WHIEpuRdmz3b9G/z+P1fL0SluPrk/Jg9Iacdz5OKjNUcRntL8Of7z1ioApbjllAGY1D+5xX3z9s8HsGR3ERJ7D8XsUwe43PbY5sUyKhyP/mI4+qR4/8HOwaJqlRkrtUZh9uxTtSsb6oDKXBgqcgFTOGw9xvv0c+tZdYR6oKOn8eQ/aHmNGamd8IZCFGqkfEwyK4F6bn+YOHGiy9eVlZUqm/Ltt986Agf5tF8CjJaMGTPGcVkCETkIzs/Pb/P2bN++Heedd57LddOmTVNd3mQNkRwgSxAjGSgJouQk2RsJ4iRAkyBJghspyZKD8QsvvFBlqggqyM3Ly3O5Tr6W18pdNkdIVk9OTUmQ0p5Apb2P99Z++egdwIjsBJw6rPmB26oDpSrQ2VdY7bI9h4urUWPWPoltelvz56hxXN5XVIPTRzTe90BxY2lTWZ2lxe9TVqs9X0qkDacMzWzxvkOzE1WgI8GBzWByKbvbW6h94nvSkIxmP7OUxxk+2qgyTVUNQFpU43OU11tdfqZThjn/HLUu29l024qrtXKa04dnoW9qy4HOyB6HVaAjz3HiEKfnsL9WA9PjcNrwLLdBctXe1vdNW0hJ0p++3KpKnAqqLejpVAK1r7DG5efvkRLX7DUf3SvR7e+VvwzLPoiC3YXq+SYPTG/2egzKjHc8v7f7Z2tOpQp0Smsamt2v1P46Tuib2q6fSyrGJNDZW1jV7Dn2SkANYPaYnmr/+aq2QZ7jqPp9d34OyeDvcTxHD4zqmdjivtmWW6kCnX1Frt+npKoehZX16vKvJvRBXKSH0MFqBaqLgIoc7VR+DPU1xxARthpZdcUwvvEQTFW52n10A04Drmi9S6Y73v7uB32gIyUtKbHhKK4y42hpLVITfC8xICL35NM/b8vHuqqma1f+8Ic/qE/cpZxN2g/LAbAEC/X12h98b//4yr6xyhuAn0kWZ926dapNspRPSamcBGbSCS4pKUltu5S7yW2SVfrzn/+MlStXqkX4oW7KlClqTZYz2V9yfbCST77F4Ez3NWODM7WD1932+zV9nHZbRYvP4fxY58c1va2oquX/Q8X22+O8OI7JSohSB16VdQ04UFSFIU4/X+PPHOd2PUpSdDhKqs3q+dLiGoPYIvtBnfoeBU1+jrzKZtupqzVbHGshUlpZyyQGZcTjh+35zfa5/vXgjObb3VFkHYVkoOS5d+dVuAQ6e5xed7l96qA0N9vaQi2iTj7JVwfAOS4Hw9rlXO1yVQFgbb5m5W2LFQ2RNoTNNQDzGoPZ6VYrtkfaYCo0AH/Rrpd3orMtFpg2t/wh2I1WG66JtMK41QDscC3Ler3BCmukDRFzjcD3vi99mA4btkdagSLA9heTWvclpDBsaYMFiASi3vXhwzrJuEXEAhFxOM0Ui48iGlBdEAPbpwNgiIwDIuNRjWhcUHcMlaZoDCmoAaqTYDBFIaHmEFB6CIhNVo9HWITLa+j8O67/H4hCHcYmVCPu2HLttao4Zn8dj9lfO/vraXVdNyPf+VL90KDA6QZTJJCQDcR3XHCs695HJl6S/7BaoFODMZ5L64koBEjpmmREWvPzzz+rEjHJkugZngMHDqCzSPMD2Yam2yQlbCaT9sYopQeyqF5ODz30kApwFi5cqErWJMCSDJCcJAiS7I+U3jmXXwULeW2k6YJz+2hpGy3lh7KeSsrOjh49qtZZiZtuugmvvPIK7rnnHlxzzTVqn3388ccqexes9IP+QR4OnvWDnL0tBDp55XUorzUjwSn74em+TQMd56+LnQIJd/RAKM6LIxT5PR+YEYeNh0vVc+iBjiwil/d8MSjd/c8swYgEOiqwcVqy5RzANDvoayFg07+OMBk9f+rtRH8tPO2rzgx01PNlxqnARZ7/1KEZjqxASwGsfG2AFSMT64CcTU2CF6egRq6rKfZ52+Q3LlyiBPnMyOrmeokc7H0D5Eu191vpIyD3CWvyWJ0Ke+W2dvYJkPApWo9unJ7D4OH6NjFXq8BQtnWyHqdtaVxfJR/dPaD/V/3yTcfPfJpc2HG/a9ARGYdTw+IwJwKoLo6C7b2+MEjQUpGDsSVHsSOqQirXgH95sV2x6UB8NpDQQ51/tseClYWRmD11PE6dOFa7LTpZC9Y6QcgEOpuPljv+6BFR6JJOaZLZkKAlLi7OY7Zl8ODB+Pzzz1UDAjmYkrUyHZGZ8US6qx1//PFqbZA0I5BOYXJwLp3WhHR227dvH04++WRVkiYZCtm+oUOHqp9vwYIFqmQtIyNDfV1QUKCCp2AkjRlkJo5OD+auvPJK1RBCSg+dSw4lqyVBzZ133qkaVPTq1QtvvfVWULeW1rMxngKdAemxjoP1oso6pNozHE2zOHJgO76P+xJI5wNiuSwHyfpaD+esQNNMSFPFVdqi2rhw75pnDLYHOioosS9R2GvPxKTFRSDZQ3YlNTZSlQ813R79+VvLTDnfzzmAkwDKm25oeiDTPIvm4bWSOjtzDVBVgti6PCBnI2CtBeoqgboKoKF9xzjnW3MRZ8pH0o6NQKy2pkkyZTPrtgEm7eB86D4r8F2E+iTfVp6DV/P3Iz2yGBHfehkROD7Jz3Y6GM5qvByXCRibH5puPFKGWz9Yh8z4KHx2c2Pm9cp3VqvX+ukLRmPaQC3TZG5owKJFi9TfhPAW1uhsOlKGWz5Yh55JUfjoBtds7vQXf0St2YoPbzgBvZp0MmurK99epUrXpGvhVPs2frnhKJ6btwsnDEjBcxc2rgX1ms0C1Fdrr3t9Jf7y+SpUlJfg+knpGJRoU9fvOpyLHQePom+cFWMzwoC6ctjqKlFfUYgImGFosJdhWuqA6jqEowgj9IBpz07HU+n/e+qNUYhI6ukIYLTX0f766dfJ62fPEOm2/m8bPs7bj3hbf5yaOQKdLSQCnYFx9ehlyMfR0uYtJIkotEhJmhwAjxgxQq25kfbSnpoByKf90tEsLS0Nf/zjH71e/OgP48ePV1kGycZIsCPtpaVhgmSZhGRvJBCTcrXa2loVmP33v//FyJEj1fqeJUuWqPU8ss2SzZE2122ZNdPdhsC21FFOgh13j1m/fj1CQVmNWWVjWgp0pPRU2t8eKalRB/eNgU6lo9RL1rXsyXMf6JRW16OgQnsOo0F7zoLKOmTER6nXZlde20vXYr1cftIYMDiVWNmfz9PPK5JjwxGOBlSVSClOY2BSV3oM6SjRvqgCSvIOITkmQv0cpfmHkW7/CN5SZgIqGlNBFYWF6nEDouO1ch5nNqt2YFpfoR2c1lViaFUZfmtaidjqWtR+vxRR1hrY6ipwS8EeRIdXY9LScGBJjTqQlYNUFdDYLCqLoZqjb4NfzZSTfPOj9pOUyEpHO+fXQf4ErtQuyh7rafD0Sb4ELz2aHwz7+El+n4h6HLEdxpFyoDKmp8qYye/j8uJtqLdFo3f/4UCyfWmC2YyayHQgqY/UEnv8nrENlep7llWHAcl9XUoQ99RrDSgSsgcCHjKY3orPLsKRghxsrkrCVPvzbKiQ505Hco/+Ls/tq5xeqfh2cw4GpwzHoJO0RgLvf7UF/9pzEDeOHoCxs7UPuRrMZsydM0d1FA2XoMYeKOnB8p8/Wo6SkiLcNi0LI3okqdfvj/MLMeeAAX/+5ST8enLbt9VTWWxnCf5AZ/37uHvjLZgQNg4fljbvNENEoUVKvyQ74kwPHppmfqSkydmtt97q8nXTUjb9YNs581NaWurzwfoFF1ygTu6ceOKJan2OO5K5mTt3rlfPS8FPz0rIehZPZWd6wCCBjhyQTB6Qqi1mtgcM8snzz3uKPK7T0Z+jR2KUaghwoKhaPVYCHVnILIGPtxmdFkvX5P+IfBItB2b2oOF4w2GcZtyAXkfWAWs2qgO3rK37cX/YMYyrDQM++Yf9/vbMh/30am0FwqLMwCJpH9b4FDKp6vdRTs/5d+1MDs8Xyz/6bYcBaDNnlakAVsttZa7XeyJ3/Yv+ctj/JMm3nyX/SHWqa78MFw3GKJhiEmGITFBlR7ImA2HR7SoHqqhtwMr9RQg3GXDykHQYYMCRkmrV2U4CPZlLVIEYnDllPKJTemFbZQzuX1CEyJRe+O+d5zX7JN+fJCsn66hkJpKUV47tnaQaZdQ3WBEVbkTPJjNqvKHPhKqoa0BdgwWRYSaX3z/ZD/FelCC2RisLzXFb2unV2iYv6AG9c6mlHlgM8hTsm8KBmBTtZFfeKwJzio5hTNwwjDhuoLpuyccLUIFaDM7ybVv1DyL25LW8xq+jBH+gkzZYnY00HmDpGhERhZy9LSzKdyaNCqRdsH4QJlkgOQiUbM704Zn2QMf9p7KOg6rMeLVGRQIdfeF60+CoxUCnpgTZ5ZswyLQPZ5Ufhenj94Gyw/aMhv3TZ6vrogZpTvuOHLNKk7VvtOuk0fQ0OcKRZSHFXhwAGRoXhFtsNhVP6YxGg1prIVdJFqHxMbLGozGwUMtHpJubATC5Czhk8bgEJLIA3B6crMkx40ClCWMG9MSQ3tnYV2HEO2sKERefhD+ed7x2v4h47XHqchzMxkjM+W6u9qm8Hzv2hZstuOHBuWo9+apzzkBGQhTe+GoL/n34IG4cNwDfbMxRx1E9h0/BpP4pWL50P9bZtuFMadXfgUGO8wGzBDq77YGO/jsn3enkd7StJOjXM5UlVWZkJZp8KkH0JaPhKCVt5f9k25/DtXFESw1IWsyO2gOmilqz6gwoBqX7FujogdaxslpVCunN+jV/Cv5AJ3MkbAYjMlGK+hL3U4aJiDqaLIB/77333N7229/+Fq+//nqnbxOFhtbW53haHK+f902NwcgeiW7XrLhbQC8ZnR+25zmu0wOtIZlxqoStuKoGKDkIFO4GCne5nqoKoFahyfG7JENbSohKwBARB1tkHLYUWlFhjcLYQb0QG5+ML7eVIac2HLMnDEbf7EyXQAH2LMhHm0rw+A9HcMro/vjbZcc7vu0v/vqTmiivb+9VU/vh4V+MxDtL9+PRb7Y5ro+PCsPmhxrXdT07dwf+vniv4/7e+O6bbfjn0v24Nr0/Hpg+Agt/2of/WLZjdu8sYPgE9w/yciJ8W0WFm1RL7P2FVeq1k0DHOfMwOLNCBTpynQQ6LXW16wjyPMv3FTX7/Wzt99oTCWAlUyXBU1FVHbIStVRdcbUe6DRvJ+8L5/9XkiWVDw9aKyVt73OU1zQ4SkkHteE5HNkX+xo3vQV2RnwkEmN8C6qTYiLU7CXZHj0b15mCP9CJiIUlZRDCinahZ91u1YkltpOjSSIiWV8j64Pc8WXgJJG3Wi1haVr+Yg+M9HM5+NEPgKS0rbq+oVk7eeeWyPosmwN5hUDuZkTuXIDbTZtwhqEUYRF70N+QA7zsOatzzJaCvdYeyEjPwqDjZ8CUPgiISm4s0bIHODBqzyOfud9rD07emDhBlV3dtXouJPlywfQzgHjnOrRGUWlHUY4SFFZZ3Gac5GBeAhr9gFr/GfXrpdRLSqf0n1fPBOglUd5o2pDAcfDuoVNcR5PsiAQ6jdm4xtdVTot3Fjh+Lzw2TeggjQfzzX8/fSWvlQQ6zllGvclEW17HlvRLjVWZI8lm5JbXOjIkMry3pVLStpDW4JLUKq/VAhwZbCuyE7X2623ex3kVWse9PP+8xvIayXbp2bjOFBJH/MbsMUDRLow0HFSfRjj32Sci6gzS/UxORJ1NL0NpbT2AfjAjnzbLmhrnGSnaGgk5KKzH3vyqxgGHauJmIeJyVuBS0wGcduAHxJTvw08RW9EzpxB43YaLnTM09q5ONlMEDCkDgfQhQJp+Goz8iN6Y+txKdWD4XB8zBkycDZMX5VlyICWBjmxz75QYFeQkRocj3Wk+TlP6rBvng1w5uNPXaEzqn4r3VhxyyiBoB30T+ibjv6sOayVP1fXITNACKf1xKXFtCHQyXdcvOJcABoJsj2TjJIhwbjAhLbz13x89a6A3mOjsQKdZUNiOdS7ufgf0OUrezELyhgTC/VJjVHZEtjmntNav63OErC+SgGqfPUiV9Uu+vDZ9U2MRZjSgqt6iAjJ/tTqX7Vi21/Mav44UEoGOLWsMsOVTjDLuVwvrGOgQEVEocJ4n09rBiny6LA0L5FNnOcDRGxHoB0vyab+hch8qNn0N7DoEHF0LHFuv5qM4ys3sncDi7QGNNSoZm2ozsaMhCydPnYrn1tmwtiodf7v5PIzp0zh4Uld4TOtsKMM8jQbvS7T0dQh77IGO/vO2tMZCP5B17gInB3iSpRGT+2uLtGV/yPwg/cBejiGSY8JV0CcHxXqg40smQF/3IOsXZD2E/gl6Z8/QcbdGw7nBhGQFBjrdpjeYkN0rvxeds23avjpUXI2aeotfSuf0oNR5SKwe9Pgr0NG3XQId2Xc5Zfb5Tn5+jeX7qUAnrwKHS/T/8/FtD8rSYtW+VcNj/RR4NzYk6PzOa6ER6GRqjfVHGg7gR/uLT0REFOz22WvsW5on40wOGrVApwLH8vIwxbgLJxzbBOzain8UrkRSVD6wyvUxNhhwyJqOw6ZeOPGEqSozc8v3lVhenoqnLzoNN/xnrbrf1jNmYfvu5ThYWY6iGvczqRoPMttW0qMfbMsnxr3tHbhaO5CUOTpCsjL6zB+9/Ew6eUkAowd+K/YWuRzYy0GwHOw7ZwJk+Ghb13bIugd9/cKKfcWq9EhKkKQUKRCcszZND3L1/Sn7Y/0hrf12n5QYtbanM8jvcFJMOEqrzVi6pxDV9RbVGa2vPbD1hR6Uyu+ATn9N/VW65th3W7VsVEcGOvO2STausjHQyYzzKShRr39ehV/KA7Vti3dZ+9OZQiejI/8hjQUoLJR+jf0CvUlEREQdTj9QafVT94Y6IHcLLsVcnB++GifOP4iLrIdhjLABq7W7SGW91WZATkRf9Bx5ItDzOKDHeHx+JB6//2IXpg5MxYmzTlD3rd60CiXlBZi7NdcxuFvWx+oHj3pA0ZQsCvfl03T9gE7K6noleVdSJXN0hJSgyeJtCTr059eDIPkecmA/d4v2c/RO1g7sG7NBjUNDZdCqT9tuX7/w3ZYcR/lQZwUPTQ3MaBwcu3p/sctBrpQCyroSKW3UX9fOzDxJICrPt/pAiWNfSUAYZtKnXLadu6yeLyWIXpco5lc41uj4e981PkelWkvn63MMzojDdwC2HC1r1/dxt22SjZM5RZ35+x0SgQ6iElFoSkeapQCGvC2SkA70FhEREXW4xhazTgcqVovW4ezoOnv52ToV5Ehf4dlyuxyDyLGeAcgxZCB7+FSg5wRswUBc8r8aZMSnYdH5pzq+3a5N25sdDOkL1+dvy3MJOtytiXCmXy/dsNpCPtWXT/drzBb8vLfQ/jPHt7quQUqyZJG4BCwS6DQtW5LtluzB/O15Lj+jHgjp9zdbrCob4/xYbw22r19ouq8CwXlwbNOfWbscrwKdxm3t3KUA8nwS6OjP3951Lo7fR3ela238HWyJ/ppuz6lQv29tbfvsDX1fbDtWrjq7OT9vW+gZvAU78tUSPNlH+gBhX8kHHFLuKVnPvQWVji6OnSE0Ah0A+RH9kFZTgMQSP48SJiIi6qJ251agJwow3XoUmPexFtzkbNDm0TQVk4rS5DF492AyNlgHYpN1IMYNG4S3L9ZaL2eU16LqfwtwsKjK5VPZpmt5nC9LZzLng2V3n6D7o3RNPtUfkBaHnXkVzZ6zJbI9cuApzzsg3enTfPt26gGi/j31uSdNA7YS+7mUncn6Il8OLB3PEcBAR39+CXTcbY8e+AVqW5v+XrX3+d0F3h2xRketbzPAEeTIgb8/v7/zc+hBjgxYTfIhWNM7/jn2sR/WYEk2Tl4rCVIl49SZgY7v+b5upiymrzrPrN4V6E0hom6sX79+eOmll7y6r8lkwpdfftnh20Tk1o/P4NkDF+DnqNtx6qa7gWV/BQ4u1YKc8Fig7zRg6u+AC98Bbt8E3L0Xtt98jJcaLsRi63EoRoJLsCBrSRKiwlRHM2lB3Lx9deMn1E0/6dcPSB2la04lX84cgYYvB2hO2xobYVKtdVvTNPBquj6j6UGe/nXTxxU5ZaJkPkubtrvJcwSqEYGn53cXwHq6byC3zRctlSCm+rF0TT4UkLJHnd7YwZ+iI0yqRLS9r82AdK1Vtc5fQ031vwl6F8jOEjIZndq4vkARMNiyt9PrA4mIiDqVzQbbkueQjDqYbSYgaxTCe09QJWiyrgbpQwFj8/dB5zbSTQ8k1RqJzHisPViigpvh2Qmq+5U+s8O5PK7ZAbGeCYlrpXTNubWv+1jIo6YH5N5MtU/1kJlpzOi4Bmz61/pBsH7/9mQBmi4Y92fbYV84P78Et85ZgaYHzx1xwN6mfdXOg/CWSxD9MzDUed/JGhX9ckeQ7+tYV+PjvokKN6kmEweK/Lutjs5rHoYOdxRjSAU68p/ScAzHCooCvTlEREQdp74SBosWKZxqeBNhN/0InPMicNxvgcwRboMcdwFDswN9p4GCQurtpY5f6u+du1TpC9ebtlFuGlg01TiVvn0Bg7drR5qWLjVdiK7WJzhtS9O1Rk0zOr5st75+oWlDgEBx/gS/6UGu8++DZA/aMozSH6QLnv6c/uhOp79epTVmx1wkX0sQ27Nf/cX59WnPcwxy+v/jr8Bb///Z2bN0QibQqY9IQpEhGUaDDWX71wd6c4iCixzp1FcF5iTP7aU33ngDPXr0gNXq2tr2vPPOwzXXXIO9e/eqy5mZmYiLi8Pxxx+PH374wW+7afPmzTj99NMRHR2N1NRU3HDDDaisbPx0a/HixZg0aRJiY2ORlJSEadOm4eDBg+q2jRs34rTTTkN8fDwSEhIwYcIErFmzxm/bRkGmSluQX2OLQFZmtlfZDXcHNk0zM47J6fY2sRLo6I9p+hz698mIj1QL/Z0/JS/28xqdptvt7afZTeeouGstrP/M+jwZdwFScTvKnbRuYtq2SyMAaQgQSC6BbpPX3znwC8RaIn2th5ABmdJQoj30AFPeRiTIKW5HCWLbfj87JmvnmtX0/Tmc//+0N2vm+D727ZFMkT6rqjOETOmaOBw5BKm1K9FwdAOAWYHeHKLgYa4GnugRmOf+0zEgwrtP9S666CL87ne/w6JFi3DGGWeo64qLizF37lzMmTNHBR2zZ8/G448/jsjISPz73//Gueeei507d6JPnz7t2syqqirMmjULU6ZMwerVq5Gfn4/rrrsOt912G9599100NDTg/PPPx/XXX4///ve/qK+vx6pVqxwHj5dddhmOO+44/P3vf1drfzZs2IBwLybGU3AprKzDZW+uxEUTe+G6kwZ4vmO11hq4GPFt/mRXP1iSNS5NP7HXb5N2y6Me+t5xwOKuhElfuO588OV1M4KYCGiTWrzXLy1GfRIva4i8XUDddM1QY2bGKRuVEYeV+4tdfsamJU/tXcAu33vVgeKANyLQB8fqbaTdbY9sa9H+wG2rPO+Gw6V+KZuTJhb6bB55DV1KJ/2spbVOXe05Btn//8RHhqkPKvxBfqf0LocHiqrU4N3OEDIZHVGSMEydRxZKi2kiCjXJyck466yz8MEHHziu+/TTT5GWlqayJWPHjsWNN96IUaNGYfDgwXjssccwcOBAfP311+1+bnnO2tpaFTzJ95fMziuvvIL//Oc/yMvLQ3l5OcrKynDOOeeo5xw+fDiuvPJKR4B16NAhTJ8+HcOGDVPbJkGbbC+FFpltIp3Fvlh/tOU7Vmsl2sW2eJcFyt6YNihVtWo+ZUh6s9vG9U5SZWkSTMgBS71FC3ROGZLW7L4nD0lTXaCcv49+ACkdnZp+qutcOuTLgaZ8uj9tUBrio8JwXB+Z+tM6PaBpbEagz8Jp/BBB3/5Th2Y0m8Ej2yvb7S5Aagv9Odzt80A4dUiG+h2YMjC1+W1D09XretLg5q95Z/D3vnIE35X17SpBbM2wrHhVeifn/goemhqelaACihHZCWqtna8mD0hRQ3NPVq+1we/ZuM5cpxNSGZ3a9JFAPpBcpvX8JyI/CY/RMiuBeu42kMyIZE1ee+01lbV5//338etf/xpGo1FldB5++GF8++23yMnJUVmWmpoaFWS01/bt21VgImVpOilNkzI6yRidfPLJuOqqq1TWZ8aMGSqoufjii5Gdna3ue9ddd6kMkARGcpsEOhIQUWjRW8fKvJgWVWulayW2eMS0cR2FlLysf3Cm6lzWlCxMX3bv6WrApU4Ggcqi9aZOH5aJTQ/NRHxUY9Ag6x70rIsECZkJjZ3RSqvrHZWo8im7L969epJqOCTb5I2ma4YaP9Fv/HlmjszC5oddfw59zo9sr2y3u5K3tjhzVPPnCKSnLhiNB84d4XYNzi2nDsLlJ/QN2LaeO7aHCrb89fzymu0rqNIyOvrr6MeOa86L/BfffSpMRoPfggd3ndd+vPu0dj9Hr+QYrLl/BmL83Ljr2QvHICE6vMMCPYR6RseUrX36mVW3H2hwnzYnIh/IH1QpHwvEqY1/zKUUzWazqWDm8OHD+Omnn1TwI/7whz/giy++wBNPPKGul/Kw0aNHqzKyzvDOO+9g+fLlmDp1Kj766CMMGTIEK1asULdJALZ161acffbZWLhwIUaMGKG2lUJLpb0jVG29xauMThESEO3DwYoc4Ho6UJIgol9arOPkLsjRNT0YlXUPepCgr4vR6QeZ0sI63Mdp93KA522Qo3eZ059bAqQq+35t+ol+059Dtk8yW/pjHe2l25EJ6CpBjpDXvqVGA4HeVn8+v/77KNk85zbhHUGCHV9/tzv7OeIiw/y/TikzXn240VGBHkI90EnOHoBSWyzC0QAUMKtDFIqioqLwq1/9SmVyZC3M0KFDMX78eHXbzz//rLIqv/zlL1WAk5WVhQMHDvjleaUUTRoKyFodnTyfZJJkG3SyDue+++7DsmXLVImbc5mdBD533nkn5s2bp34GCYwotOgDB6tby+hUOWV03GRmAsndkEahH2S2dwp7WzhndPTtkZItCba8faxsd0k7MzoUOHr2prhK1unYm0rwdQwaIRXo9EqJwVZrP3W54ejGQG8OEQWIZHAko/P22287sjlC1r58/vnnKpMjQclvfvObZh3a2vOcEmTJupstW7aohgjSGOHyyy9XXd7279+vAhzJ6EinNQlmdu/erQIkKZ+TpgXSlU1ukwBJGhrIbRSagY7Mr/F2jU5XmxvnbkhjR02k93Zb6hqsjvkj8mm+N584Owdsgdh28o/G17GOr2MQCqlARyL0HYb+6nL1wbWB3hwiChBpBJCSkqLWxkgwo3vhhRdUwwIpHZMSN1kvo2d72ismJgbff/+96vImbasvvPBC1flNGhLot+/YsQMXXHCBytxI6+lbb71VNUeQLmtFRUW44oor1G2ydkeaKjzyyCN+2TbqPmQRv35gLovgWw10kNDlMjqNn6C7z+h05kGm7JvIMKPLfA9vn79xEXudo4kCMwHdj3NDCkeg04lZRepYIdWMQD6hyYkZAtQCyGFGhyhUSbnYsWPNmyf069dPrX9xJsGGs7aUslksFvVcOimHa/r9dZLV8bTmJiIiQpXZEVXZMzqixUX3ThkdWaDclegBgl7upQtE+ZccF8jzHSurdXSC8nYhun6/fYVVqrlCe9foUGC4K19kwBo8QiqjI6pSRqrz6OLtgLWV1D8REVEXLF1rtfOac6DT5UrXXFs66wJVNqQPDdUDHW9bROvbqT+uPU0UKHBYghjcQu5/ZI8Bo1Bli0S4tRYo2hPozSGibkqaGcTFxbk9SeaGqCO7rrW6TsfejEAGhna1jE7Tls6BLF1zDmx259kzOl6XrjV5HMuduiX9902G8ZZUm9VlZnSCR0iVronx/dKwfUlfTDTsAnI2AemN3Y6IiLz1i1/8ApMnT3Z7m6ypIerIOTotZnQsDUBtqbpYbPOtvXTnNCNomtGp67AZJi3RD2pzy2vbFGj5+jjqWvTft0KnducsQQweIRfoyFTnT239MBG7UHlwDeLGXBToTSKibig+Pl6d3JFObeXl5Z2+TRT8Kuu0T5xFtaeMTk2xOrPaDChDbPdpL+1mWGdnbk9bD3Kb3o+BTvfUdGYOSxCDS8i9krJwsyRhmLpce2hDoDeHqFuTwZvU/fF1DLLSNfv6HAlyLDB12fbSTQMdx/qIDhrW2Nr26LwtW2p6v87ebvIP+f8R6/RhAAPW4BJygY6I6HWcOo8t3irv8IHeHKJuJzxcm0pdX+96oELdU3V1tcvrSl03IHVtRtB42e36HFs8ZLC53j65q9ADBGnJrLfIlp9Nb9GsNwfoLE0PbNvaXtrxdSdvN/mP82vHQCe4hFzpmug19DjU7zAh2lIBlB4CkvsGepOIuhVZg5KQkICCggI1BFMW4HszYC9USOmaBIG1tbUu7aW7Gjm4lCAnPz8fSUlJXFvUxcnsHLOl8cO5mnprKzN0tI5rXe3/pl7yJZ8zllbXq0X85bUNjp9NC4T8M6i3IzM6vj6Ouh4plzxcXBOQ0knqWCEZ6Izvn4mdtt4YbTiAusPrEclAh6jNMjIysGvXLkRGRqKwUPsEmRoDiJqaGkRHR3e5g0x3JMjJysoK9GZQK5yzOaK6vqGV1tIJiPY0ZyeAZP2DrIOQ4EbK1STQ0cvWZD2RlBKZzZ0X6DQrQfMyYNFLnqrsJYTMBHRfzr8DDFiDS9f7C9gJeiZFY23YQIy2HkDB7lXoNeb8QG8SUbcjB/AVFRWYOnVqoDelyzGbzViyZAlOPvnkLl8OJtvHTE73W5+jDwxtfVho18wo6lkc6bw22KnjWiCCBefnlM8lktqw1kZKnqocmQAeIHdXzq8dSxCDS1ioHqDVpI4CChag4SgbEhC1hxwkd/WD+UDsk4aGBlXWx31DHZXR8dheuknpWlc9sNxfWIUSeyanuCpw80tSnUqVpAOXSRY2+VDy5Px9qHthRid4dc2PejpBTL8J6jypbHugN4WIiKhVFU0yOh7bSzs1I4iO6JqfZzadpRPIjE5CdBjC7MFNW5/f+aCYmYAgyegw0AkqIRvo9B0xCRabAUmWYljLcgK9OURERH7N6JRIoBPeNd/m9QBBX5ujBzyBWAguVR56g4S2HuQ635+ZgO6LgU7w6pp/ATvBiD6Z2Iee6nLurlWB3hwiIiKvh4W2PEfHntFBAmK6eEZHD3SK7cNC9Sn1nU0PUtoarOj315soUPfk/HvHEsTgErKBjnR9yY2WJZBA0e7Vgd4cIiKiFlXWWbwMdIobS9e66MG3HujkltWqFtO55bVup9R3Fv159cyO14+z3z9Q203+4fz6JcdyXWUw6Zof9XQSc+YY4OAiIHdjoDeFiIioTV3Xqt2VrslwGkczggQMcZr43hUDnblbc9Up0OVf+vqatj6//nMEKhNF/uGcxWFGJ7iEbEZHJPbXGhKkVuwM9KYQERF5VbqmZ2lq3WV06quAhtoun9GZ1D8FaU2CAwkyJg9ICcj2zBieqZ7/5CHpbXrcCf1TkR4fiVkjOYeqO+uRFIWxvZNw2tB0RHfRDwfINyGd0Rk0dhqwGOhhy8OeQ4cxqE/vQG8SERFRixmdjIRIHCyqdt+MwJ7NaTBEoBqRau1IV9QrOQar/jQdVslA2RkNBhjb0NrZn84/rifOG9ejzQN++6TKz3FGtxgMTJ6FmYz48papfB2DUGhndJLTUBCmfQqzbuWPgd4cIiIijyrsXdfS4yI9t5e2NyKoCkuSfmJdeoG8BDVygKmfAhXk6Hw9yOXBcXDg6xicQjrQEeaMMeq8cPdq2Jw+WSIiIuqqGR1R6zajozUiqDAlqHOW4RBRKAv5QCdt0ER13qNmFzYfLQv05hAREbU4R6fljI5WulZhTFTnXbV0jYioM4R8oBPRe7w6H2k4iP9tPBbozSEiImox0MlIiFLnbtfoVGmla6XQMjpduXSNiKhLBTpPPvkkjj/+eMTHxyMjIwPnn38+du5svWPZJ598gmHDhiEqKgqjR4/GnDlz0GVkj1VnAw3H8MPG/bBaWb5GRERdt3RNz+jUtJDRKbEHOszoEFEoa1Og8+OPP+LWW2/FihUrMH/+fJjNZsycORNVVVUeH7Ns2TJceumluPbaa7F+/XoVHMlpy5Yt6BLiMmCLy4LRYENyxS6sOVgS6C0iIiLy3IzAvkZHMjrN1pbamxGUIF6dd9X20kREXS7QmTt3Lq666iqMHDkSY8eOxbvvvotDhw5h7dq1Hh/z8ssv48wzz8Tdd9+N4cOH47HHHsP48ePxyiuvoKsw2LM6I40HWL5GRETdIqNjsdpQb7G6bUZQaI1T52xGQEShrF1zdMrKtMX7KSmeB3wtX74cd911l8t1s2bNwpdffunxMXV1deqkKy8vV+eSQZJTW+mP8fRYY8YomHZ/j5GGA3hm8zH86czBqtVlKGht34Q67h/PuG9CY98Ew88QDBosVseaHL3rmqittyIyzNRsjU6BlRkdIiKfAx2r1Yo77rgD06ZNw6hRozzeLzc3F5mZmS7XyddyfUtrgR555JFm18+bNw8xMTG+brIqt3Mnu9SMSQDGmA6guMqMv370PYYlhdZaHU/7hjTcP55x3wT3vqmurg70JpDEL3WN63GSoiMQZjSgwWpTwU8iwput0clviFXnMREhPReciEKcz38BZa2OrLNZunSpf7cIwH333eeSBZKMTu/evdV6oIQEbYFlWz+RlAOOGTNmIDzc6Q1BVzoKePWvGGI4gnA0wJAxBLOnD0IoaHXfhDjuH8+4b0Jj3+gZdQqsynqtbC0yzIiIMKPK1MianWr79U0DndwGe+kaMzpEFMJ8CnRuu+02fPPNN1iyZAl69erV4n2zsrKQl5fncp18Ldd7EhkZqU5NyQFDew4aPD4+bQAQlYSw2lIV7BRX9+/2Bydt1d59G+y4fzzjvgnufdPdtz/Y1ufER4U51t5IoOPSYtpqAWq0hjo5Zi2jExURGmXYRETutOkvoHR3kSDniy++wMKFC9G/f/9WHzNlyhQsWLDA5Tr5pFOu7zIMBiB7jLo40rgfhZWN64OIiIgCrbJOWysVF9kY6DRrMa2CHK3sutimZXRYukZEoczY1nK19957Dx988IGapSPrbORUU1PjuM8VV1yhSs90t99+u+rW9vzzz2PHjh14+OGHsWbNGhUwdSl65zXDARRUMNAhIqKuo8Ke0YnVAx17SZpLRsfeiMAalQQLtNtZukZEoaxNgc7f//531Wnt1FNPRXZ2tuP00UcfOe4j7aZzcnIcX0+dOlUFRm+88YZqSf3pp5+qjmstNTAIiOxx6myUkYEOERF1LZX2GTpNMzrVzhkd+/oca5TWCVXW8piMhs7fWCKiLqJNOe1mg8ncWLx4cbPrLrroInXq0rK00rXhhkMorqxVP6tBStqIiIi62BqdGHugU+uc0bEPCzXbAx1mc4go1HGVoi51IGzhsYgx1KGn9SjKa5p0siEiIuoqGZ1wzxkdc0SySzBERBSqGOjojCYYskY1rtOprA30FhEREbms0YlzdF0La96MwB7o1EZqgQ4zOkQU6hjouGlIIOt08rlOh4iIulxGR2v3HR1udNOMwB7ohCWp8ygGOkQU4hjouFmnM8qwnw0JiIio687R0buuucnoVIclqnOWrhFRqGOg467FtPEAChnoEBFRl+26Zi9dc9OMoNKU5NKZjYgoVDHQcZY+DBZDGBIN1agvPBDorSEiojZ49dVX0a9fP0RFRWHy5MlYtWpVi/d/6aWXMHToUERHR6N379648847UVvbNddnVrShGUGFScvocI0OEYU6BjrOwiJQFDtIXYwu2hLorSEiIi/JPLe77roLDz30ENatW6fmts2aNQv5+flu7y/z3e699151/+3bt+Of//yn+h5/+tOf0BVV1ppdmhG4by9drM7KDfZAhxkdIgpxDHSaqEweqc6Ty7cHelOIiMhLL7zwAq6//npcffXVGDFiBF5//XXExMTg7bffdnv/ZcuWYdq0afjNb36jskAzZ87EpZde2moWKFCq6rSAJt6e0YlyDAx1GoVQpZWulSJenXONDhGFujYNDA0F5szRwOHPkF29K9CbQkREXqivr8fatWtx3333Oa4zGo2YPn06li9f7vYxU6dOxXvvvacCm0mTJmHfvn2YM2cOLr/8crf3r6urUyddeXm5OjebzerUVvpjvH1shT2jIwkdeUyk/WPK6roG7XuYqxHeUKOuK7TGyS2IMBl82rauoK37J5Rw37SM+yc09o3Zy5+BgU4TYT3GqfMBDXsCvSlEROSFwsJCWCwWZGZmulwvX+/YscPtYySTI4878cQTYbPZ0NDQgJtuuslj6dqTTz6JRx55pNn18+bNU5kjX82fP9+r+5VUSnbGgLUrfsbhaGB7kQGACcfyClWAFl1fiJmAWme6cW+OdtuhA5gzZx+6M2/3TyjivmkZ949nwbBvqqurvbofA50m4vuOhcVmQJqhFJayHJgSswO9SURE5GeLFy/GE088gddee001LtizZw9uv/12PPbYY3jggQea3V+yRbIGyDmjIw0MpOQtISHBp08j5WBjxowZCA/XZuN4IoHYnSu0A5OzZ56B9PhIxO4qwDu71iMqPgGzZ08BcjYAWwFjbBqyevUB8o5i1LAhmH3qAHRHbdk/oYb7pmXcP6Gxb8rtWfXWMNBpIiUpCXttPTDEcBSVB9YhcezZgd4kIiJqQVpaGkwmE/Ly8lyul6+zsrLcPkaCGSlTu+6669TXo0ePRlVVFW644Qb8+c9/VqVvziIjI9WpKTlYaM8BgzePl3U4Vpt2OTkuGuHhJsRFa9tSa7Zqj68rU18bYtNR26DdOTaqfdvWFbR3/wYz7puWcf94Fgz7xtvtZzOCJsJMRuwxaZ+A1R1eF+jNISKiVkRERGDChAlYsGCB4zqr1aq+njJliseyh6bBjARLegalKw4L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IaAJ46KGixK8LOMeYYLKEBAEGQEJFwHHWEDY5THP17S0rC00v5/BnvN+JaVrz0nDedL20+/3fPtUMjIy5NFHH5VTp06F7WOtwu23j9LT093jTJs2Terq6sRL5s6d637VPPQHbf1em+PHj8sTTzzhjj8tLU169erlfvsqwPqXWHv522+/3W3Pz8+XgwcPhj3GuXPnpKioyP0gWWZmpowfP15qamokkdXX18usWbOkc+fO7rjvuusumT17tquH32uD+JFNZFMsZFM4cik6suk61MOWL1+uqampumjRIt27d68+/fTTmpmZqadOnVIvKygo0MWLF+uePXt0165d+tBDD2nHjh21pqYmuM+ECRO0Q4cOWlZWpjt37tR7771XBw4cGNxeV1enPXv21Pz8fP3pp5903bp1mpWVpS+99JJ6xffff6+dOnXSe+65R6dMmRK838+1OXfunObk5OjYsWO1vLxcDx8+rN98840eOnQouM/cuXO1ZcuWunr1aq2oqNChQ4dq586d9dKlS8F9hgwZor1799bt27frli1btEuXLjpq1ChNZMXFxdq6dWtdu3atVlZW6meffaYZGRn6/vvvq99rg/iQTWRTLGRTOHIpNrIpNk8PdAYMGKDPPfdc8HZ9fb22a9dO33rrLfWT06dP27BeN23a5G5XVVVp06ZN3Ysh4JdffnH7bNu2zd22N8jk5GQ9efJkcJ/58+drixYt9PLly5roLly4oF27dtXS0lJ94IEHgmHi99rMmDFD77vvvqjbGxoaNDs7W995553gfVazW265RZctW+Zu79u3z9Vrx44dwX2++uorTUpK0uPHj2uievjhh/Wpp54Ku2/EiBFaVFSkfq8N4kM2/YNsaoxsaoxcio1sis2zS9euXLkiP/zwgzs9F5CcnOxub9u2Tfzk/Pnz7rpVq1bu2upSW1sbVptu3bpJx44dg7Wxazs13LZt2+A+BQUFUl1dLXv37pVEZ6f/7fR+aA2M32vzxRdfSP/+/d0P/Nqyhz59+siCBQuC2ysrK+XkyZNh9WnZsqVbehNaHzvtbY8TYPvb66+8vFwS1cCBA6WsrEx+/fVXd7uiokK2bt0qhYWF4vfa4MaRTVeRTY2RTY2RS7GRTbGliEedOXPGrVsMfcEbu71//37xi4aGBrfGd9CgQdKzZ093nz3hU1NT3ZP62trYtsA+kWoX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"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 40
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 评估"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-20T14:54:38.317745Z",
     "start_time": "2025-01-20T14:54:20.266675Z"
    }
   },
   "source": [
    "# dataload for evaluating\n",
    "\n",
    "# load checkpoints\n",
    "model.load_state_dict(torch.load(\"checkpoints/monkeys-resnet50/best.ckpt\", weights_only=True,map_location=\"cpu\"))\n",
    "\n",
    "model.eval()\n",
    "loss, acc = evaluating(model, val_loader, loss_fct)\n",
    "print(f\"loss:     {loss:.4f}\\naccuracy: {acc:.4f}\")"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loss:     0.1664\n",
      "accuracy: 0.9926\n"
     ]
    }
   ],
   "execution_count": 41
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "pytorch",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.8"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
